Dart
Présentation
Introduction à Dart pour Flutter
Dart est un langage de programmation open source développé par Google, initialement conçu pour le développement web, mais qui a trouvé une application majeure avec Flutter, le framework de Google pour la création d'applications multiplateformes (iOS, Android, Web, Desktop). Dart est un langage orienté objet avec une syntaxe qui rappelle C, Java et JavaScript, ce qui le rend relativement facile à apprendre pour les développeurs familiers avec ces langages.
Pourquoi Dart a été choisi pour Flutter ?
Plusieurs raisons ont motivé le choix de Dart pour Flutter :
- Performances: Dart compile en code machine natif (AOT - Ahead-of-Time) pour les versions release des applications, ce qui offre des performances proches du natif sur les appareils mobiles. En mode développement, il utilise la compilation JIT (Just-in-Time) pour un rechargement à chaud (Hot Reload) très rapide, ce qui accélère considérablement le cycle de développement.
- Productivité: Le Hot Reload permet aux développeurs de voir instantanément les modifications apportées au code dans l'application en cours d'exécution, sans avoir à la redémarrer. Ceci améliore grandement la productivité et l'itération rapide.
- Gestion de l'interface utilisateur (UI): Dart est optimisé pour la création d'interfaces utilisateur réactives. Son système de mise en page basé sur les widgets et son exécution rapide permettent de créer des animations fluides et des interfaces utilisateur complexes.
- Facilité d'apprentissage: Sa syntaxe familière le rend accessible aux développeurs venant d'autres langages.
- Support de Google: Étant développé par Google, Dart bénéficie d'un support et d'une évolution constants, ce qui garantit sa pérennité et son adaptation aux nouvelles technologies.
Comparaison rapide
Avantages
- Hot Reload plus performant: Bien que Kotlin et Swift proposent des fonctionnalités similaires, le Hot Reload de Dart est souvent considéré comme plus rapide et plus fiable, ce qui est un atout majeur pour le développement rapide d'interfaces utilisateur.
- Compilation AOT et JIT: Dart combine les avantages de la compilation AOT pour la performance en production et de la compilation JIT pour le développement rapide.
- Framework intégré (Flutter): Dart est intrinsèquement lié à Flutter, ce qui permet une intégration plus profonde et une optimisation conjointe.
Inconvénients
- Écosystème plus restreint: L'écosystème de Dart, bien qu'en croissance, est plus petit que ceux de Kotlin (avec l'écosystème Java) et de Swift (avec l'écosystème Apple). Cela signifie qu'il peut y avoir moins de bibliothèques et d'outils disponibles pour certaines tâches spécifiques.
- Moins utilisé en dehors du mobile: Kotlin est largement utilisé pour le développement backend (avec Spring) et Swift gagne en popularité pour le développement côté serveur. Dart est principalement associé au développement mobile avec Flutter, ce qui limite son champ d'application.
- Adoption moins large: Kotlin est le langage privilégié pour le développement Android natif et Swift pour iOS. Dart n'a pas cette même position dominante dans un écosystème natif.
Tableau comparatif simplifié
| Caractéristique | Dart | Kotlin | Swift |
|---|---|---|---|
| Principal usage | Développement mobile multiplateforme (Flutter) | Développement Android natif, backend, multiplateforme | Développement iOS/macOS natif, backend |
| Compilation | AOT et JIT | AOT (Android natif), JVM bytecode | AOT |
| Hot Reload | Très rapide et fiable | Disponible, mais parfois moins stable | Disponible |
| Écosystème | En croissance, plus restreint | Très large (basé sur Java) | Large (basé sur l'écosystème Apple) |
| Courbe d'apprentissage | Relativement facile pour les développeurs familiers avec C, Java, JavaScript | Facile pour les développeurs Java | Relativement facile, syntaxe moderne |
Hello World
void helloDart(String name) {
print('Hello, $name');
}
void main() {
List<String> greetings = [
'World',
'Mars',
'Oregon',
'Barry',
'David Bowie',
];
for (var name in greetings) {
helloDart(name);
}
}
Entrées/Sorties (I/O)
import 'dart:io';
void main() {
stdout.writeln('Entrez le nom');
String? input = stdin.readLineSync();
return helloDart(input);
}
void helloDart(String? name) {
print('Hello, $name');
}
https://api.flutter.dev/flutter/dart-io/Stdin/readLineSync.html
Principes de base
- Dart est un langage orienté objet et supporte l'héritage unique.
- En Dart, tout est un objet et chaque objet est une instance d'une classe. Chaque objet hérite de la classe Object. Même les nombres sont des objets, pas des types primitifs.
- Dart est typé. Vous ne pouvez retourner un entier d'une méthode qui devrait retourner une chaîne.
Types de base
// Équivalent à String name = 'Voyager I';
// Il s'agit d'une inférence de type (détection du type)
var name = 'Voyager I';
var year = 1977;
var antennaDiameter = 3.7;
var flybyObjects = ['Jupiter', 'Saturn', 'Uranus', 'Neptune'];
var image = {
'tags': ['saturn'],
'url': '//path/to/saturn.jpg'
};
Null safety (Dart >= 2.12)
- Par défaut, un type est non-nullable :
String name = 'A';ne peut pas devenirnull. - Le suffixe
?autorisenull:String? maybeName;. - Opérateurs null-aware :
user?.name,value ?? fallback,counter ??= 0. lateretarde l'initialisation d'un non-nullable que l'on promet d'assigner avant usage (sinon erreur runtime).requiredforce un paramètre nommé non-nullable à l'appel.!force une valeur nullable en non-nullable ; à limiter, préférer tester ou utiliser??.
String formatUser(String? raw) {
final cleaned = raw?.trim();
return cleaned == null || cleaned.isEmpty ? 'Anonyme' : cleaned;
}
class User {
final String id;
final String? avatarUrl;
const User({required this.id, this.avatarUrl});
}
Conversions de types : is, as, cast
is/is!testent un type sans lancer d'exception et promeuvent le type dans le bloc.ascaste et peut lancer si le type réel ne correspond pas.cast<T>()retypise une collection existante et échoue au premier élément incompatible (utile quand une API renvoieList<dynamic>).
void logLength(Object value) {
if (value is String) {
print(value.length); // promotion de type
}
}
num sumIfNums(Object a, Object b) {
final numA = a as num; // peut throw si a n'est pas num
final numB = b as num;
return numA + numB;
}
final raw = <dynamic>['a', 'b'];
final strings = raw.cast<String>(); // erreur runtime si un élément n'est pas String
Contrôle de flux
if (year >= 2001) {
print('21st century');
} else if (year >= 1901) {
print('20th century');
}
for (final object in flybyObjects) {
print(object);
}
for (int month = 1; month <= 12; month++) {
print(month);
}
while (year < 2016) {
year += 1;
}
Fonctions
optionalReturnType functionName(optionalType parameter1, optionalType parameter2...) {
// code
}
int fibonacci(int n) {
if (n == 0 || n == 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
var result = fibonacci(20);
Paramètres nommés
void named({String? greeting, String? name}) {
final actualGreeting = greeting ?? 'Allô';
final actualName = name ?? 'Personne mystère';
print('$actualGreeting, $actualName!');
}
named(greeting: 'Bien le bonjour');
named(name: 'Sonia');
named(name: 'Alex', greeting: 'Bonjour');
// Bien le bonjour, Personne mystère!
// Allô, Sonia!
// Bonjour, Alex!
Ceci sera particulièrement utile à l'intérieur de cadriciel Flutter:
Container(
margin: const EdgeInsets.all(10.0),
color: Colors.red,
height: 48.0,
child: Text('Named parameters are great!'),
)
Imaginez la même situation sans les paramètres nommés : on devrait se rappeler pour chaque constructeur l'ordre des éléments. L'avantage ici, c'est que même mélangés, ils restent valides.
Closures
L'idée ici est de mettre une fonction dans la définition d'une autre fonction.
// Ceci définit un type nommé NumberGetter qui représente une fonction retournant un entier
typedef NumberGetter = int Function();
// La méthode suivante va recevoir une fonction comme argument et retourner le carré de cette méthode
int powerOfTwo(NumberGetter getter) {
return getter() * getter();
}
void consumeClosure() {
final getFour = () => 4; // fonction anonyme qui retourne 4
final squared = powerOfTwo(getFour);
print(squared);
}
Symbole =>
Ce symbole permet de spécifier une fonction inline :
final getFour = () {
return 4;
};
final getFour = () => 4;
Classes
Points principaux :
- Pas de mots-clés
public/protected/private. Le préfixe_rend un membre privé à la bibliothèque. - Toute classe peut être utilisée comme interface via
implements. - Pas de
structséparé : pour des données légères, utilisez une classe immuable (ou des records en Dart 3).
class Person {
final String first;
final String last;
const Person({required this.first, required this.last});
String toString() => '$first $last';
}
Héritage, interfaces et mixins
Héritage : permet à une classe d'hériter des propriétés et méthodes d'une autre classe (classe parente). La classe enfant réutilise le code de la classe parente et peut le surcharger ou l'étendre. Dart ne supporte que l'héritage simple (une seule classe parente).
Interface : définit un contrat que les classes doivent respecter en implémentant toutes les méthodes et propriétés spécifiées. En Dart, toute classe peut servir d'interface. Contrairement à l'héritage, on ne réutilise pas l'implémentation : il faut tout réécrire.
Mixin : bloc de code réutilisable (méthodes et propriétés) que l'on « mélange » dans une classe pour lui ajouter des fonctionnalités sans passer par l'héritage. Utile pour partager du comportement entre plusieurs classes qui n'ont pas de relation hiérarchique. Un mixin n'a pas de constructeur et peut imposer des contraintes avec on.
Mots-clés :
extends: pour hériter d'une classeimplements: pour implémenter une interfacewith: pour appliquer un ou plusieurs mixins
class Animal {
void speak() => print('...');
}
mixin Walker {
void walk() => print('marche');
}
// Dog hérite d'Animal et applique le mixin Walker
class Dog extends Animal with Walker {
void speak() => print('woof');
}
// FriendlyDog implémente l'interface Animal (doit réécrire speak)
class FriendlyDog implements Animal {
void speak() => print('happy woof');
}
Différence clé entre extends et implements :
Avec extends Animal, la classe Dog hérite de l'implémentation : si Animal contient du code dans speak(), Dog peut l'utiliser directement ou le surcharger avec @override. C'est une relation parent-enfant.
Avec implements Animal, la classe FriendlyDog s'engage à respecter le contrat : elle doit obligatoirement réécrire toutes les méthodes/propriétés d'Animal depuis zéro. Elle ne récupère aucun code existant, juste la signature. C'est utile pour garantir qu'une classe respecte une structure commune sans imposer une hiérarchie.
Opérateur cascade
Création d'un patron de conception bâtisseur qui crée une URL.
Sans l'opérateur cascade
class UrlBuilder {
String? _scheme;
String? _host;
String? _path;
UrlBuilder setScheme(String value) {
_scheme = value;
return this;
}
UrlBuilder setHost(String value) {
_host = value;
return this;
}
UrlBuilder setPath(String value) {
_path = value;
return this;
}
String build() {
if (_scheme == null || _host == null || _path == null) {
throw StateError('scheme, host et path doivent être définis');
}
return '${_scheme}://${_host}/${_path}';
}
}
void main() {
final url = UrlBuilder()
.setScheme('https')
.setHost('dart.dev')
.setPath('/guides/language/language-tour#cascade-notation-')
.build();
print(url);
}
Avec cascade
class UrlBuilder {
late String scheme;
late String host;
List<String> routes = const [];
String toString() {
final paths = [host, ...routes];
final path = paths.join('/');
return '$scheme://$path';
}
}
final url = UrlBuilder()
..scheme = 'https'
..host = 'dart.dev'
..routes = [
'guides',
'language',
'language-tour#cascade-notation-',
];
print(url);
L'opérateur cascade n'est pas utile seulement dans ce cas ; il peut aussi manipuler des éléments :
// Au lieu de faire ceci:
final numbers = [342, 23423, 53, 232, 534];
numbers.insert(0, 10);
numbers.sort((a, b) => a.compareTo(b));
// On fait ça:
final numbers = [342, 23423, 53, 232, 534]
..insert(0, 10)
..sort((a, b) => a.compareTo(b));
print('The largest number in the list is ${numbers.last}');
Enum
enum PlanetType { terrestrial, gas, ice }
enum Planet {
mercury(planetType: PlanetType.terrestrial, moons: 0, hasRings: false),
venus(planetType: PlanetType.terrestrial, moons: 0, hasRings: false),
earth(planetType: PlanetType.terrestrial, moons: 1, hasRings: false),
mars(planetType: PlanetType.terrestrial, moons: 2, hasRings: false),
jupiter(planetType: PlanetType.gas, moons: 95, hasRings: true),
saturn(planetType: PlanetType.gas, moons: 146, hasRings: true),
uranus(planetType: PlanetType.ice, moons: 27, hasRings: true),
neptune(planetType: PlanetType.ice, moons: 14, hasRings: true);
const Planet(
{required this.planetType, required this.moons, required this.hasRings});
final PlanetType planetType;
final int moons;
final bool hasRings;
bool get isGiant =>
planetType == PlanetType.gas || planetType == PlanetType.ice;
}
final yourPlanet = Planet.earth;
if (!yourPlanet.isGiant) {
print('Your planet is not a "giant planet".');
}
Gestion d'erreurs
try/on/catch/finallypour intercepter et nettoyer.catch (e, s)récupère aussi leStackTrace(s) pour loguer ou réémettre.- En async, combinez avec
await; pour propager après log, utilisezrethrow.
Future<int> safeDivide(int a, int b) async {
try {
return a ~/ b;
} on IntegerDivisionByZeroException catch (e, s) {
// log(e, s);
rethrow; // laisse l'appelant gérer
} catch (e, s) {
// log(e, s);
throw Exception('Division impossible');
} finally {
// cleanup éventuel
}
}
Pour aller plus loin / Conclusion
Dart est un langage puissant et performant, particulièrement adapté au développement d'applications mobiles multiplateformes avec Flutter. Ses avantages en termes de productivité (Hot Reload) et de performances en font un choix pertinent pour de nombreux projets. Bien que son écosystème soit plus petit que ceux de Kotlin et Swift, il continue de croître et de s'améliorer. Le choix entre Dart, Kotlin et Swift dépendra des besoins spécifiques du projet, de l'expérience de l'équipe de développement et des plateformes ciblées.
Ressource : https://dart.dev/guides/language/language-tour